﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Manoso.Data.FluentMap
{
    public interface IMapUpdate<T> : IMapCommand<T>
    {
        /// <summary>
        /// Indicate whether Id is included in the object or db should generate the id.
        /// </summary>
        bool IncludeId { get; set; }

        /// <summary>
        /// Map a column to a property of upper level object type.
        /// </summary>
        /// <typeparam name="TAny">The upper level object type of the hierarchy map.</typeparam>
        /// <typeparam name="TP">The upper level property type.</typeparam>
        /// <param name="alias">The table alias of TAny.</param>
        /// <param name="column">The column name.</param>
        /// <param name="lambda">Lambda expression of the property, given the upper level type.</param>
        void Map<TAny, TP>(string alias, string column, Func<TAny, TP> lambda);

        /// <summary>
        /// Map a column to a property of current object type.
        /// </summary>
        /// <typeparam name="TP">The property type.</typeparam>
        /// <param name="column">The column name.</param>
        /// <param name="lambda">Lambda expression of the property, given the current type.</param>
        void Map<TP>(string column, Func<T, TP> lambda);

        /// <summary>
        /// Set object properties of given map type with return values of the sql update/insert command.
        /// </summary>
        /// <typeparam name="TAny">Any upper level type in the hierarchy map.</typeparam>
        /// <typeparam name="TP">The property type.</typeparam>
        /// <param name="alias">The table alias of TAny.</param>
        /// <param name="column">The column name.</param>
        /// <param name="lambda">Lambda expression to set the property with the returned value.</param>
        void Return<TAny, TP>(string alias, string column, Action<TAny, TP> lambda);

        /// <summary>
        /// Set object properties of current map type with return values of the sql update/insert command.
        /// </summary>
        /// <typeparam name="TP">The property type.</typeparam>
        /// <param name="column">The column name.</param>
        /// <param name="lambda">Lambda expression to set the property with the returned value.</param>
        void Return<TP>(string column, Action<T, TP> lambda);
    }

    internal class MapUpdate<T> : MapCommand<T>, IMapUpdate<T>
    {
        protected virtual MapCommandType CommandType
        {
            get { return MapCommandType.Update; }
        }

        public bool IncludeId { get; set; }

        public MapUpdate()
        {
            IsMatch = NOT_MATCH;
        }

        public override void GetCommand(IMapTask task, IList<string> stack)
        {
            var sub = (T)task.Values[TableAlias];
            if (!IsMatch(sub)) return;

            if (ActionFix != null) ActionFix(sub);

            var command = GenerateCommand(task.Adaptor, task, stack);

            if (string.IsNullOrEmpty(command)) return;

            var returnCmd = MapReturns.Count <= 0 ? null : GenerateReturn(task.Adaptor, task);
            if (MapReturns.Count <= 0)
                task.AddCommand(command, CommandType);
            else
            {
                if (task.Submit == null) return;

                LoadReturns(task);
                task.Submit(string.Join(string.Empty, stack) + command + (returnCmd ?? string.Empty), () => ReadReturns(task));
            }
        }

        public void Map<TAny, TP>(string alias, string column, Func<TAny, TP> lambda)
        {
            if (string.IsNullOrWhiteSpace(column) || lambda == null) return;

            var mp = new MapProperty
            {
                HostAlias = alias,
                PropertyType = typeof(TP),
                Column = new MapColumn(column, Alias)
            };

            mp.Load = x => mp.Value = lambda((TAny)x);

            AddMap(mp);
        }

        public void Map<TP>(string column, Func<T, TP> lambda)
        {
            Map(TableAlias, column, lambda);
        }

        public void Return<TAny, TP>(string alias, string column, Action<TAny, TP> lambda)
        {
            if (string.IsNullOrWhiteSpace(column) || lambda == null) return;

            var map = new MapProperty
            {
                HostAlias = alias ?? typeof(TAny).Name,
                PropertyType = typeof(TP),
                Column = new MapColumn(column, Alias),
                Set = (x, p) => lambda((TAny)x, (TP)p),
            };

            MapReturns.Add(map);
        }

        public void Return<TP>(string column, Action<T, TP> lambda)
        {
            Return(Alias, column, lambda);
        }

        protected virtual string GenerateReturn(IMapAdaptor adaptor, IMapTask task)
        {
            var sub = (T)task.Values[TableAlias];

            if (IsTable && IsMatch(sub) && MapReturns != null)
            {
                LoadIds(task);
                var ids = Keys;
                if (ids.Count() == 1 || CheckTstamp && ids.Count() == 2 && MapTstamp != null)
                {
                    var id = ids.First(x => x != MapTstamp);
                    //return adaptor.GetReturn(Target, id.ColumnName, null, id.Value);
                    //var idParam = id.Column.Alias;
                    var columns = MapReturns.Select(x => x.Column).ToArray();
                    return adaptor.GetReturn(Table, TableAlias, columns, id.Column);
                }
            }
            return null;
        }

        protected virtual string GenerateCommand(IMapAdaptor adaptor, IMapTask task, IList<string> stack)
        {
            LoadProperties(task, !IsTable || IncludeId);
            if (Columns.Count <= 0)
                Columns.AddMany(CreateProperties());

            var properties = Columns.Select(x => x.Column);
            //var propCols = adaptor.GetColumns(properties);
            //var propVals = adaptor.GetValues(properties, references);

            if (!IsTable)
            {
                return adaptor.GetExec(Target, properties);
            }

            LoadIds(task);
            var ids = Keys.Select(x => x.Column);
            //var idCols = adaptor.GetColumns(ids);
            //var idVals = adaptor.GetValues(ids);

            var update = adaptor.GetUpdate(Target, ids, properties);
            return update;
        }

        protected void ReadReturns(IMapTask task)
        {
            foreach (var map in MapReturns.Where(map => map.Set != null && map.Host != null && task.HasColumn(map.Column.Name)))
            {
                map.Set(map.Host, task.ReadColumn(map.Column.Name));
            }
        }
    }
}